package jeme;

import java.util.HashMap;

import jeme.lang.SchemeBoolean;
import jeme.lang.SchemeObject;

/**
 * An environment is a sequence of frames maintaining the bindings of
 * variable names to their values. Procedures use an environment to
 * lookup variables.
 * 
 * @author Erik Silkensen (silkense@colorado.edu)
 * @version Sep 19, 2009
 */
public class Environment 
{   
    /**
     * A default global environment. This should only be used by
     * procedures that are created and added to an environment before
     * compilation begins. For example, the standard library procedures
     * use this environment.
     */
    public static final Environment DEFAULT = new Environment();
    
    private HashMap<String, SchemeObject> env;

    private Environment parent;
    
    private static int instances = 0;
    
    private int id;

    /**
     * Creates a default environment with no parent. 
     */
    public Environment() 
    {
        this(null);
    }

    /**
     * Creates an environment with the specified parent environment. 
     * 
     * @param parent  the frame one level up from this one
     */
    public Environment(Environment parent) 
    {
        this.env = new HashMap<String, SchemeObject>();
        this.parent = parent;
        this.id = instances++;
    }

    /**
     * Returns the value for the variable with the specified name. If
     * no binding is present for the specified name, <code>null</code>
     * is returned.
     * 
     * @param key  the variable name to lookup
     * @return  the value for the variable with the specified name
     */
    public SchemeObject get(String key) 
    {
        Environment scope = this;
        while (scope != null && scope.env.get(key) == null) {
            scope = scope.getParent();   
        }
        
        if (scope == null) {
            throw new RuntimeException("Unbound variable: " + key);
        } else {
            return scope.env.get(key);
        }
    }

    /**
     * Binds the specified variable name <code>key</code> to the 
     * specified value. The previous value bound to <code>key</code> is
     * returned, or if no binding was present, <code>null</code> is
     * returned.
     * 
     * @param key the variable name to bind
     * @param value the value for the variable with the specified name
     * @return the previous value of the variable with the specified
     * name
     */
    public SchemeObject put(String key, SchemeObject value) 
    {
        Environment scope = this;
        while (scope.env.get(key) == null && scope.getParent() != null) {
            scope = scope.getParent();
        }
        
        scope = scope.env.get(key) == null ? this : scope;
        
        SchemeObject prev = scope.env.put(key, value);
        return prev == null ? SchemeBoolean.UNSPECIFIED : prev;
    }
    
    /**
     * 
     * 
     * @param key
     * @param value
     * @return
     */
    public SchemeObject sput(String key, SchemeObject value)
    {
        return env.put(key, value);
    }

    /**
     * Returns the parent of, or the frame one level up from, this
     * environment.
     * 
     * @return  the frame one level up from this one 
     */
    public Environment getParent() 
    {
        return parent;
    }
    
    /*
     * (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    public String toString()
    {
        return "env[" + id + "," + parent + "]";
    }
}
